<html>

<head>
  <meta charset="UTF-8">
  <title>NNVM WebGL Test Page</title>
</head>

<body>
  <h1>NNVM WebGL Test Page</h1>

  <!-- We will draw the input image here. -->
  <div>Input Image:</div>
  <img id="image", src="data.png">

  <!-- We need a canvas to get the image pixel data. Hide this element. -->
  <canvas hidden id="image_canvas" width="224" height="224"></canvas>

  <!-- We will write te prediction result here. -->
  <div id="prediction"></div>

  <!-- We will write all log messages here. -->
  <div id="log">Log:</div>

  <!-- The OpenGL canvas. -->
  <canvas id="canvas"></canvas>

  <script>
    var Module = {};

    // resnet.js would recognize Module["canvas"]
    Module["canvas"] = document.getElementById("canvas");
  </script>

  <script src="resnet.js"></script>
  <script src="tvm_runtime.js"></script>

  <script>

    /**
     * Load a text file synchronously.
     * @param {string} url The file path.
     * @return {string} The file content.
     */
    function load_file(url) {
      assert(typeof url == "string", "URL must be string");

      var req = new XMLHttpRequest();
      var result;
      req.addEventListener("load", function() {
        result = this.responseText;
      });
      req.open("get", url, false);
      req.send();
      return result;
    }

    /**
     * The index of the maximum element in an array.
     * @param {Array} The array.
     * @return {number} The index.
     */
    function argmax(arr) {
      assert(typeof arr.length == "number", "Input must be array-like");

      var res = 0;
      for (var i = 0; i < arr.length; i++) {
        if (arr[i] > arr[res]) {
          res = i;
        }
      }
      return res;
    }

    /**
     * Preprocess an image to fit resnet input format.
     * @param {ImageData} The input image data. Should be 224x224xRGBA.
     * @return {Float32Array} The preprocessed input array.
     */
    function preprocess_image(image_data) {
      assert(image_data instanceof ImageData, "Input must be ImageData.");
      assert(image_data.width == 224, "Width must be 224.");
      assert(image_data.height == 224, "Height must be 224.");

      var width = image_data.width;
      var height = image_data.height;
      var npixels = width * height;

      var rgba_uint8 = image_data.data;
      assert(rgba_uint8.length == npixels * 4, "Image should be RGBA.");

      // Drop alpha channel. Resnet does not need it.
      var rgb_uint8 = new Uint8Array(npixels * 3);
      for (var i = 0; i < npixels; i++) {
        rgb_uint8[i * 3] = rgba_uint8[i * 4];
        rgb_uint8[i * 3 + 1] = rgba_uint8[i * 4 + 1];
        rgb_uint8[i * 3 + 2] = rgba_uint8[i * 4 + 2];
      }

      // Cast to float and normalize.
      var rgb_float = new Float32Array(npixels * 3);
      for (var i = 0; i < npixels; i++) {
        rgb_float[i * 3] = (rgb_uint8[i * 3] - 123.0) / 58.395;
        rgb_float[i * 3 + 1] = (rgb_uint8[i * 3 + 1] - 117.0) / 57.12;
        rgb_float[i * 3 + 2] = (rgb_uint8[i * 3 + 2] - 104.0) / 57.375;
      }

      // Transpose. Resnet expects 3 greyscale images.
      var data = new Float32Array(npixels * 3);
      for (var i = 0; i < npixels; i++) {
        data[i] = rgb_float[i * 3];
        data[npixels + i] = rgb_float[i * 3 + 1];
        data[npixels * 2 + i] = rgb_float[i * 3 + 2];
      }

      return data;
    }

    // Set these variables at the global scope so that we can debug more easily.
    var tvm;
    var syslib;
    var graph_json_str;
    var loaded_module;
    var data_array;
    var data;
    var input;
    var base64_params;
    var output;
    Module["onRuntimeInitialized"] = function () {
      tvm = tvm_runtime.create(Module);

      tvm.logger = function (message) {
        console.log(message);
        var d = document.createElement("div");
        d.innerHTML = message;
        document.getElementById("log").appendChild(d);
      };

      tvm.logger("Loading SystemLib...");
      syslib = tvm.systemLib();
      tvm.logger("- SystemLib loaded!");

      tvm.logger("Loading resnet model...");
      graph_json_str = load_file("resnet.json");
      ctx = tvm.context("opengl", 0);
      loaded_module = tvm.createGraphRuntime(graph_json_str, syslib, ctx);
      tvm.logger("- Model loaded!");

      tvm.logger("Loading model parameters...");
      base64_params = load_file("resnet.params");
      loaded_module.load_base64_params(base64_params);
      tvm.logger("- Model parameters loaded!");

      tvm.logger("Loading input image...");
      var image = document.getElementById("image");
      var image_canvas = document.getElementById("image_canvas");
      var image_canvas_context = image_canvas.getContext("2d");
      image_canvas_context.drawImage(image, 0, 0);
      var image_data = image_canvas_context.getImageData(0, 0, 224, 224);
      data_array = preprocess_image(image_data);
      tvm.logger("- Input image loaded!");

      tvm.logger("Setting input data...");
      data_shape = JSON.parse(load_file("data_shape.json"));
      data = tvm.empty(data_shape, "float32", ctx);
      data.copyFrom(data_array);
      loaded_module.set_input("data", data);
      tvm.logger("- Input data set!");

      tvm.logger("Running model...");
      loaded_module.run();
      tvm.logger("- Model execution completed!");

      out_shape = JSON.parse(load_file("out_shape.json"));
      output = tvm.empty(out_shape, "float32", ctx);
      loaded_module.get_output(0, output);

      prediction = argmax(output.asArray());
      
      synset = JSON.parse(load_file("synset.json"));
      result_string = "Prediction: " + synset[prediction] + "\n";
      document.getElementById("prediction").innerHTML = result_string;
    };

  </script>
</body>

</html>
